﻿using System;
using System.Collections.Generic;
using System.Linq;
using Ipmce.RemoteVoting.Common.Tools.CardReader.Exceptions;
using Subsembly.SmartCard;

namespace Ipmce.RemoteVoting.Common.Tools.CardReader.Utils
{
    class PassportReaderImpl : IDisposable, IPassportReader
    {
        private const int Slot = 0;
        private readonly ICardTerminal _cardTerminal;
        private const byte BufferLength = 220;
        static readonly byte[] Iv = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
        private byte[] _sessionKeyEnc;
        private byte[] _sessionKeyMac;
        private byte[] _ssc;
        private static readonly byte[] Do8EBase = new byte[] { 0x8e, 0x08 };

        public PassportReaderImpl(ICardTerminal cardTerminal)
        {
            _cardTerminal = cardTerminal;
        }

        public void Dispose()
        {
            _cardTerminal.DisconnectCard(Slot);
        }

        public byte[] GetRawDg2Data(string mrz)
        {
            Initialize(mrz);
            SelectEFile(0x02);
            var length = SelectLength();
            var result = new List<byte>();
            for (var i = 0; i < length; i += BufferLength)
            {
                var lengthToRead = i + BufferLength < length ? BufferLength : (byte)(length - i);
                result.AddRange(SelectBinary(lengthToRead, new[] { (byte)(0xff & i), (byte)((0xff00 & i) >> 8) }));
            }
            return result.ToArray();
        }

        private int SelectLength()
        {
            var lengthArray = SelectBinary(4, new byte[] { 0x00, 0x00 });
            return lengthArray[2] << 8 | lengthArray[3] + 4;
        }

        private byte[] SelectBinary(byte length, IList<byte> offset)
        {
            var command = CryptoUtils.Padding(new byte[] { 0x0c, 0xb0, offset[1], offset[0] });
            var do97 = new byte[] { 0x97, 0x01, length };
            var m = command.Concat(do97).ToArray();
            IncrementSsc();
            var n = _ssc.Concat(m).ToArray();
            var cc = CryptoUtils.ComputeInBookMac(n, _sessionKeyMac, Iv);
            var do8E = (Do8EBase).Concat(cc).ToArray();
            var response = _cardTerminal.SendCommand(Slot,
                                                     new CardCommandAPDU(0x0c, 0xb0, offset[1], offset[0], do97.Concat(do8E).ToArray()));
            if (response.IsError)
            {
                throw new CardReaderException("Unable to read binary!");
            }
            var rapdu = response.GetData();
            IncrementSsc();
            var do87HeaderSize = GetDo87HeaderSize(rapdu);
            var do87Length = rapdu[do87HeaderSize - 2];
            var do87 = rapdu.Take(do87HeaderSize + do87Length - 1).ToArray();
            var do99 = new byte[] { 0x99, 0x02, response.SW1, response.SW2 };
            var k = _ssc.Concat(do87).Concat(do99).ToArray();
            var responseMac = CryptoUtils.ComputeInBookMac(k, _sessionKeyMac, Iv);
            if (!rapdu.Skip(do87.Length + do99.Length + Do8EBase.Length).SequenceEqual(responseMac))
            {
                throw new CardReaderException("Response MAC ain't correct!");
            }
            return CryptoUtils.RemovePadding(CryptoUtils.Decode(do87.Skip(do87HeaderSize).ToArray(), _sessionKeyEnc, Iv).ToArray());
        }

        private void Initialize(string mrz)
        {
            byte[] attr;
            var result = _cardTerminal.ConnectCard(0, CardTypes.T1, out attr);
            if (result != CardActivationResult.Success)
            {
                throw new CardReaderException("Unable to open card: " + result);
            }
            var response = _cardTerminal.SendCommand(0, new CardCommandAPDU(0x00, 0xa4, 0x04, 0x0c, new byte[] { 0xA0, 0x00, 0x00, 0x02, 0x47, 0x10, 0x01 }));
            if (response.IsError)
            {
                throw new CardReaderException("Unable to do something");
            }
            response = _cardTerminal.SendCommand(0, new CardCommandAPDU(0x00, 0x84, 0x00, 0x00, 0x08));
            if (response.IsError)
            {
                throw new CardReaderException("Unable to get challenge");
            }

            var rndIcc = response.GetData();
            var rndIfd = CryptoUtils.CreateRandomBytes(8);
            var kIfd = CryptoUtils.CreateRandomBytes(16);

            var s = rndIfd.Concat(rndIcc).Concat(kIfd).ToArray();

            var kseed = CryptoUtils.ComputeKseed(mrz);

            var kEnc = CryptoUtils.ComputeKey(kseed, new byte[] { 0x00, 0x00, 0x00, 0x01 });
            var kMac = CryptoUtils.ComputeKey(kseed, new byte[] { 0x00, 0x00, 0x00, 0x02 });

            var eFid = CryptoUtils.Encode(s, kEnc, Iv);
            var mFid = CryptoUtils.ComputeInBookMac(eFid, kMac, Iv);
            var cmdData = eFid.Concat(mFid).ToArray();

            response = _cardTerminal.SendCommand(0, new CardCommandAPDU(0x00, 0x82, 0x00, 0x00, cmdData));
            if (response.IsError)
            {
                throw new CardReaderException("Unable to mutual authenticate");
            }

            var kIcc = AnalyzeAuthenticateResponse(response.GetData(), kEnc, kMac, rndIcc, rndIfd);
            ComputeSessionKeys(kIcc, kIfd);
            ComputeSendSequenceCounter(rndIcc, rndIfd);
        }

        private static int GetDo87HeaderSize(byte[] rapdu)
        {
            if (rapdu[0] == 0x87 && rapdu[1] == 0x81 && rapdu[3] == 0x01)
            {
                return 4;
            }
            return 3;
        }

        private void IncrementSsc()
        {
            var index = _ssc.Length - 1;
            do
            {
                if (_ssc[index] == 0xff)
                {
                    ++_ssc[index];
                    --index;
                }
                else
                {
                    ++_ssc[index];
                    break;
                }

            } while (true);
        }

        private void SelectEFile(byte dg)
        {
            var command = CryptoUtils.Padding(new byte[] { 0x0c, 0xa4, 0x02, 0x0c });
            var data = CryptoUtils.Padding(new byte[] { 0x01, dg });
            var encr = CryptoUtils.Encode(data, _sessionKeyEnc, Iv);
            var do87 = (new byte[] { 0x87, 0x09, 0x01 }).Concat(encr).ToArray();
            var m = command.Concat(do87).ToArray();
            IncrementSsc();
            var n = _ssc.Concat(m).ToArray();
            var cc = CryptoUtils.ComputeInBookMac(n, _sessionKeyMac, Iv);
            var do8E = (Do8EBase).Concat(cc).ToArray();
            var response = _cardTerminal.SendCommand(Slot, new CardCommandAPDU(0x0c, 0xa4, 0x02, 0x0c, do87.Concat(do8E).ToArray()));
            if (response.IsError)
            {
                throw new CardReaderException("Unale to get DG2 length");
            }
            var rapdu = response.GetData();
            IncrementSsc();
            var do99 = new byte[] { 0x99, 0x02, response.SW1, response.SW2 };
            var k = _ssc.Concat(do99).ToArray();
            var responseMac = CryptoUtils.ComputeInBookMac(k, _sessionKeyMac, Iv);
            if (!rapdu.Skip(do99.Length + (Do8EBase).Length).SequenceEqual(responseMac))
            {
                throw new CardReaderException("Response MAC ain't correct!");
            }
        }

        private void ComputeSendSequenceCounter(ICollection<byte> rndIcc, ICollection<byte> rndIfd)
        {
            _ssc = rndIcc.Skip(rndIcc.Count - 4).Concat(rndIfd.Skip(rndIfd.Count - 4)).ToArray();
        }

        private void ComputeSessionKeys(byte[] kIcc, byte[] kIfd)
        {
            var kSeed = CryptoUtils.Xor(kIcc, kIfd);
            _sessionKeyEnc = CryptoUtils.ComputeKey(kSeed, new byte[] { 0x00, 0x00, 0x00, 0x01 });
            _sessionKeyMac = CryptoUtils.ComputeKey(kSeed, new byte[] { 0x00, 0x00, 0x00, 0x02 });
        }

        private static byte[] AnalyzeAuthenticateResponse(ICollection<byte> data, byte[] kEnc, IEnumerable<byte> kMac,
            ICollection<byte> rndIcc, ICollection<byte> rndIfd)
        {
            var mac = data.Skip(data.Count - 8).Take(8).ToArray();
            var encoded = data.Take(data.Count - 8).ToArray();
            if (!mac.SequenceEqual(CryptoUtils.ComputeInBookMac(encoded, kMac, Iv)))
            {
                throw new CardReaderException("Mac error");
            }

            var decoded = CryptoUtils.Decode(encoded, kEnc, Iv);
            if (!rndIcc.SequenceEqual(decoded.Take(rndIcc.Count)))
            {
                throw new CardReaderException("Icc Rnd invalid");
            }

            if (!rndIfd.SequenceEqual(decoded.Skip(rndIcc.Count).Take(rndIfd.Count)))
            {
                throw new CardReaderException("Ifd Rnd invalid");
            }

            return decoded.Skip(rndIcc.Count + rndIfd.Count).ToArray();
        }
    }
}
